Моя задача — провести оценку результатов A/B-теста. В распоряжении есть датасет с действиями пользователей, техническое задание и несколько вспомогательных датасетов.
import pandas as pd
import numpy as np
import seaborn as sns
import datetime as dt
import scipy.stats as st
import plotly.express as px
import math as mth
from plotly import graph_objects as go
import matplotlib.pyplot as plt
sns.set_style("darkgrid")
#функция расчета z-критерия для двух групп
def z_test(successes_1, successes_2, trials_1, trials_2, alpha=0.05, bonferroni_alpha = 1):
alpha = alpha / bonferroni_alpha # критический уровень статистической значимости
successes = np.array([successes_1,successes_2])
trials = np.array([trials_1, trials_2])
# пропорция успехов в первой группе:
p1 = successes[0]/trials[0]
# пропорция успехов во второй группе:
p2 = successes[1]/trials[1]
print(successes[0], successes[1],trials[0] , trials[1])
# пропорция успехов в комбинированном датасете:
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
# разница пропорций в датасетах
difference = p1 - p2
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('p-значение: ', p_value)
print('значение alpha:', alpha)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными'
)
ab_project_marketing_events = pd.read_csv('https://code.s3.yandex.net//datasets/ab_project_marketing_events.csv')
final_ab_new_users = pd.read_csv('https://code.s3.yandex.net//datasets/final_ab_new_users.csv')
final_ab_events = pd.read_csv('https://code.s3.yandex.net//datasets/final_ab_events.csv')
final_ab_participants = pd.read_csv('https://code.s3.yandex.net//datasets/final_ab_participants.csv')
ab_project_marketing_events.head(3)
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
final_ab_new_users.head(3)
| user_id | first_date | region | device | |
|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | EU | PC |
| 1 | F1C668619DFE6E65 | 2020-12-07 | N.America | Android |
| 2 | 2E1BF1D4C37EA01F | 2020-12-07 | EU | PC |
final_ab_events.head(3)
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 0 | E1BDDCE0DAFA2679 | 2020-12-07 20:22:03 | purchase | 99.99 |
| 1 | 7B6452F081F49504 | 2020-12-07 09:22:53 | purchase | 9.99 |
| 2 | 9CD9F34546DF254C | 2020-12-07 12:59:29 | purchase | 4.99 |
final_ab_participants.head(3)
| user_id | group | ab_test | |
|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test |
| 1 | A7A3664BD6242119 | A | recommender_system_test |
| 2 | DABC14FDDFADD29E | A | recommender_system_test |
dataset = [ab_project_marketing_events, final_ab_new_users, final_ab_events, final_ab_participants]
sp = ['ab_project_marketing_events', 'final_ab_new_users', 'final_ab_events', 'final_ab_participants']
sp
['ab_project_marketing_events', 'final_ab_new_users', 'final_ab_events', 'final_ab_participants']
for i, t in zip(sp, dataset):
print(i)
print()
print(t.info())
print()
print(t.isna().agg(['sum', 'mean']))
print()
print(f"Дубликатов: {t.duplicated().sum()}")
print()
print('-----------------------')
ab_project_marketing_events
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 14 entries, 0 to 13
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 name 14 non-null object
1 regions 14 non-null object
2 start_dt 14 non-null object
3 finish_dt 14 non-null object
dtypes: object(4)
memory usage: 576.0+ bytes
None
name regions start_dt finish_dt
sum 0.0 0.0 0.0 0.0
mean 0.0 0.0 0.0 0.0
Дубликатов: 0
-----------------------
final_ab_new_users
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 61733 entries, 0 to 61732
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 user_id 61733 non-null object
1 first_date 61733 non-null object
2 region 61733 non-null object
3 device 61733 non-null object
dtypes: object(4)
memory usage: 1.9+ MB
None
user_id first_date region device
sum 0.0 0.0 0.0 0.0
mean 0.0 0.0 0.0 0.0
Дубликатов: 0
-----------------------
final_ab_events
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 440317 entries, 0 to 440316
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 user_id 440317 non-null object
1 event_dt 440317 non-null object
2 event_name 440317 non-null object
3 details 62740 non-null float64
dtypes: float64(1), object(3)
memory usage: 13.4+ MB
None
user_id event_dt event_name details
sum 0.0 0.0 0.0 377577.000000
mean 0.0 0.0 0.0 0.857512
Дубликатов: 0
-----------------------
final_ab_participants
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 18268 entries, 0 to 18267
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 user_id 18268 non-null object
1 group 18268 non-null object
2 ab_test 18268 non-null object
dtypes: object(3)
memory usage: 428.3+ KB
None
user_id group ab_test
sum 0.0 0.0 0.0
mean 0.0 0.0 0.0
Дубликатов: 0
-----------------------
Пропуски есть только final_ab_events['details'], посмотрим на них
final_ab_events[final_ab_events['details'].isna()].iloc[:5]
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 62740 | 2E1BF1D4C37EA01F | 2020-12-07 09:05:47 | product_cart | NaN |
| 62741 | 50734A22C0C63768 | 2020-12-07 13:24:03 | product_cart | NaN |
| 62742 | 5EB159DA9DC94DBA | 2020-12-07 22:54:02 | product_cart | NaN |
| 62743 | 084A22B980BA8169 | 2020-12-07 15:25:55 | product_cart | NaN |
| 62744 | 0FC21E6F8FAA8DEC | 2020-12-07 06:56:27 | product_cart | NaN |
Посмотрим какие вообще есть события в event_name
final_ab_events['event_name'].value_counts(ascending=False)
login 189552 product_page 125563 purchase 62740 product_cart 62462 Name: event_name, dtype: int64
Проверим что при событии purchase нет пропусков
final_ab_events[(final_ab_events['details'].isna()) & (final_ab_events['event_name'] == 'purchase')]
| user_id | event_dt | event_name | details |
|---|
Из всех событий вычтем покупки и должны получить все пропуски
(len(final_ab_events) - len(final_ab_events[final_ab_events['event_name'] == 'purchase'])
== final_ab_events['details'].isna().sum())
True
for i, t in zip(sp, dataset):
print(i)
print()
for index in t.columns:
row = t[index].nunique()
print(f'Уникальных значений {index}: {row}')
print('-----------------')
ab_project_marketing_events Уникальных значений name: 14 Уникальных значений regions: 6 Уникальных значений start_dt: 14 Уникальных значений finish_dt: 14 ----------------- final_ab_new_users Уникальных значений user_id: 61733 Уникальных значений first_date: 17 Уникальных значений region: 4 Уникальных значений device: 4 ----------------- final_ab_events Уникальных значений user_id: 58703 Уникальных значений event_dt: 267268 Уникальных значений event_name: 4 Уникальных значений details: 4 ----------------- final_ab_participants Уникальных значений user_id: 16666 Уникальных значений group: 2 Уникальных значений ab_test: 2 -----------------
recommender_system_test;product_page,product_cart,purchase.ab_project_marketing_events — календарь маркетинговых событий на 2020 год.
Структура файла:
name — название маркетингового события;regions — регионы, в которых будет проводиться рекламная кампания;start_dt — дата начала кампании;finish_dt — дата завершения кампании.final_ab_new_users — пользователи, зарегистрировавшиеся с 7 по 21 декабря 2020 года.
Структура файла:
user_id — идентификатор пользователя;first_date — дата регистрации;region — регион пользователя;device — устройство, с которого происходила регистрация.final_ab_events — действия новых пользователей в период с 7 декабря 2020 по 4 января 2021 года.
Структура файла:
user_id — идентификатор пользователя;event_dt — дата и время события;event_name — тип события;details — дополнительные данные о событии. Например, для покупок, purchase, в этом поле хранится стоимость покупки в долларах.final_ab_participants — таблица участников тестов.
Структура файла:
user_id — идентификатор пользователя;ab_test — название теста;group — группа пользователя.print(f" Минимальная дата старта рекламной компании: {ab_project_marketing_events['start_dt'].min()}")
print(f" Максимальная дата старта рекламной компании: {ab_project_marketing_events['start_dt'].max()}")
Минимальная дата старта рекламной компании: 2020-01-25 Максимальная дата старта рекламной компании: 2020-12-30
print(f" Минимальная дата завершения рекламной компании: {ab_project_marketing_events['finish_dt'].min()}")
print(f" Максимальная дата завершения рекламной компании: {ab_project_marketing_events['finish_dt'].max()}")
Минимальная дата завершения рекламной компании: 2020-02-07 Максимальная дата завершения рекламной компании: 2021-01-07
print(f" Минимальная дата регистрации: {final_ab_new_users['first_date'].min()}")
print(f" Максимальная дата регистрации: {final_ab_new_users['first_date'].max()}")
Минимальная дата регистрации: 2020-12-07 Максимальная дата регистрации: 2020-12-23
Зацепили два дня лишних, позже удалим их.
print(f" Минимальная дата события: {final_ab_events['event_dt'].min()}")
print(f" Максимальная дата события: {final_ab_events['event_dt'].max()}")
Минимальная дата события: 2020-12-07 00:00:33 Максимальная дата события: 2020-12-30 23:36:33
По ТЗ мы должны были собирать данные новых пользователей по 04/01, по факту события прекратились 30/12.
Посмотрим распределение событий по дате
# подготовим таблицу
t = final_ab_events
t['event_dt'] = pd.to_datetime(t['event_dt']).dt.date
# строим график
plt.figure(figsize=(15,5))
g = sns.histplot(x='event_dt', data=t,)
g.set_xlabel('Дата',fontsize=12)
g.set_ylabel('Количество событий ', fontsize=12)
g.set_title('Распределение собитий по датам', fontsize=15)
plt.show()
Странно что действия прекратились 30/12 возможно или раньше остановили тест или был сбой
Посмотрим из каких регионов есть пользователи
final_ab_new_users['region'].value_counts(normalize=True, ascending=False)
EU 0.749518 N.America 0.148300 CIS 0.051107 APAC 0.051075 Name: region, dtype: float64
Вывод:
У нас есть 4 датафрейма, дубликатов нет, в одном есть пропуски, пропуски появились в столбце details для событий не связанных с оплатой. Сбор данных прекратили 30/12, по ТЗ тест должен был идти до 04/01.
Для дальнейшего исследования нам надо дату привести к формату дата и удалим пользователей зарегистрировавшихся после 21/12/2020
Дату приведем в к формату дата
ab_project_marketing_events['start_dt'] = pd.to_datetime(ab_project_marketing_events['start_dt'])
ab_project_marketing_events['finish_dt'] = pd.to_datetime(ab_project_marketing_events['finish_dt'])
final_ab_new_users['first_date'] = pd.to_datetime(final_ab_new_users['first_date'])
final_ab_events['event_dt'] = pd.to_datetime(final_ab_events['event_dt'])
df = final_ab_participants[final_ab_participants['ab_test'] == 'recommender_system_test']
len(df)
6701
Отфильтруем по группе и проверим что массивы с ID не пересекаются
np.intersect1d(df.query('group == "B"')['user_id'],
df.query('group == "A"')['user_id'])
array([], dtype=object)
Пользователи не пересекаются.
final_ab_participants['ab_test'].value_counts(normalize=True)
interface_eu_test 0.633184 recommender_system_test 0.366816 Name: ab_test, dtype: float64
Помимо нашего теста, пользователи принимали участие еще в другом тесте, давайте посомтрим пересекают ли они
len(np.intersect1d(final_ab_participants.query('ab_test == "recommender_system_test"')['user_id'],
final_ab_participants.query('ab_test == "interface_eu_test"')['user_id']))
1602
1602 пользователя принимали участи в обоих тестах.
Проверим сколько было в группах A
len(np.intersect1d(final_ab_participants.query('ab_test == "recommender_system_test" and group == "A"')['user_id'],
final_ab_participants.query('ab_test == "interface_eu_test" and group == "A"')['user_id']))
482
Проверим сколько было в группах B
len(np.intersect1d(final_ab_participants.query('ab_test == "recommender_system_test" and group == "B"')['user_id'],
final_ab_participants.query('ab_test == "interface_eu_test" and group == "B"')['user_id']))
344
В recommender_system_test в А и interface_eu_test B
len(np.intersect1d(final_ab_participants.query('ab_test == "recommender_system_test" and group == "A"')['user_id'],
final_ab_participants.query('ab_test == "interface_eu_test" and group == "B"')['user_id']))
439
В recommender_system_test в B и interface_eu_test A
len(np.intersect1d(final_ab_participants.query('ab_test == "recommender_system_test" and group == "B"')['user_id'],
final_ab_participants.query('ab_test == "interface_eu_test" and group == "A"')['user_id']))
337
Можем предположить, что пересечение пользователей никак не скажутся на нашем эксперименте.
Посмотрим проходили ли еше маркетинговые события в исследуемый нами период.
ab_project_marketing_events[(ab_project_marketing_events['start_dt'] >= '2020-12-07')
& ((ab_project_marketing_events['start_dt'] <= '2021-01-04'))]
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 10 | CIS New Year Gift Lottery | CIS | 2020-12-30 | 2021-01-07 |
В это время проходило два маркетинговых события, если одно началось 30/12 ( когда мы остановили сбор данных), то второе пришлось на наши даты, оно могло оказать влияния на поведение наших пользователей, но в равно доле.
Удалим пользователей которые зарегистрировались после 2020-12-21
t = len(final_ab_new_users)
final_ab_new_users = final_ab_new_users[final_ab_new_users['first_date'] <= '2020-12-21'].copy()
print( f" Удалилили: {t - len(final_ab_new_users)} пользователей")
Удалилили: 5263 пользователей
Объеденим df и final_ab_new_users
df = df.merge(final_ab_new_users, on='user_id', how='left').copy()
df.head(3)
| user_id | group | ab_test | first_date | region | device | |
|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC |
| 1 | A7A3664BD6242119 | A | recommender_system_test | 2020-12-20 | EU | iPhone |
| 2 | DABC14FDDFADD29E | A | recommender_system_test | 2020-12-08 | EU | Mac |
print(f"Пропусков : {df['first_date'].isna().sum()}")
Пропусков : 0
Посмотрим есть ли пользователи которые зарегистрировались после 2020-12-21
df['first_date'].max()
Timestamp('2020-12-21 00:00:00')
После объединения таблиц, остались только те пользователи, которые зарегистрировались до 21/12/2020
Посмотрим на распределение по регионам, по ТЗ должно быть 15% новых пользователей из региона EU
df['region'].value_counts().to_frame()
| region | |
|---|---|
| EU | 6351 |
| N.America | 223 |
| APAC | 72 |
| CIS | 55 |
(df['region'].value_counts().to_frame()[:1]
/ final_ab_new_users['region'].value_counts().to_frame()[:1]) * 100
| region | |
|---|---|
| EU | 15.0 |
Получилось меньше, чем планировали в ТЗ
Как раз 15 процентов.
print(f"У нас пользователи зарегистрировавшиеся в течении:{(df['first_date'].max() - df['first_date'].min()).days} дней")
У нас пользователи зарегистрировавшиеся в течении:14 дней
Посомтрим на распределение пользователей по группам
group = df['group'].value_counts(normalize=True).to_frame()
group
| group | |
|---|---|
| A | 0.570661 |
| B | 0.429339 |
В группу А попало больше пользователей
Вывод:
В тесте приняло участие 6701 человек, часть из них принимало участие в другом тесте с интерфейсом, но были там распределены более менее равномерно (350-450 чел) по 4 корзинам, делать так плохо, но думаю этого количества достаточно, что б считать, что это не скажется на результате нашего эксперимента. Так же в это время проходило маркетинговое событие, которое могло оказать влияние на тест.
Ровно 15% новых пользователей из региона EU, как и просили в ТЗ.
Пользователи в группе распределены не равномерно, но в достаточно количестве.
Проверим как распределены количество событий на пользователя
df = df.merge(final_ab_events, on='user_id', how='left').copy()
df.head(3)
| user_id | group | ab_test | first_date | region | device | event_dt | event_name | details | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-07 | purchase | 99.99 |
| 1 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-25 | purchase | 4.99 |
| 2 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC | 2020-12-07 | product_cart | NaN |
len(df)
27724
В ТЗ за 14 дней с момента регистрации пользователи покажут улучшение каждой метрики не менее, чем на 10%.
Давайте отберем этих пользователей
df = df[(df['event_dt'] - df['first_date']).dt.days <= 14]
len(df)
24070
t = df.groupby('group').agg({'event_name':'count', 'user_id':'nunique'}).reset_index()
t['quantity_per_user'] = t['event_name'] / t['user_id']
t
| group | event_name | user_id | quantity_per_user | |
|---|---|---|---|---|
| 0 | A | 18947 | 2747 | 6.897343 |
| 1 | B | 5123 | 928 | 5.520474 |
Пользователи в группе А совершают больше действий
plt.figure(figsize=(15,5))
ax = sns.barplot(data = t, x='group', y='event_name', palette='Paired')
ax.set_xlabel('',fontsize=12)
ax.set_ylabel('Количество событий', fontsize=12)
ax.set_title('Распределение количества событий', fontsize=15)
plt.xticks(rotation = 25)
plt.show()
После того, как мы установили лайфтайм, дисбаланс классов усилился.
Раньше было в процентах
group
| group | |
|---|---|
| A | 0.570661 |
| B | 0.429339 |
Стало
np.round(t.iloc[0:2, 2] / t['user_id'].sum() * 100, 2).to_frame()
| user_id | |
|---|---|
| 0 | 74.75 |
| 1 | 25.25 |
Посмортим на распредеение событий
plt.figure(figsize=(15,5))
g = sns.histplot(x='event_dt', data=df, hue='group')
g.set_xlabel('Дата собятия',fontsize=12)
g.set_ylabel('Количество событий ', fontsize=12)
g.set_title('Распределение событий', fontsize=15)
plt.xticks(rotation=45)
plt.show()
Группа А совершает больше событий, но там и больше выборка.
Посомтрим на отношение групп по дням
relation = df.pivot_table(index='event_dt', values='event_name', columns='group', aggfunc='count' ).reset_index()
relation['event_dt'] = relation['event_dt'].astype('str')
relation['percent'] = (relation['A'] / relation['B'])
relation.iloc[:5]
| group | event_dt | A | B | percent |
|---|---|---|---|---|
| 0 | 2020-12-07 | 331 | 378 | 0.875661 |
| 1 | 2020-12-08 | 341 | 252 | 1.353175 |
| 2 | 2020-12-09 | 385 | 361 | 1.066482 |
| 3 | 2020-12-10 | 350 | 263 | 1.330798 |
| 4 | 2020-12-11 | 374 | 168 | 2.226190 |
f, ax = plt.subplots(figsize=(15, 5), dpi= 250)
sns.barplot(data=relation, x=relation['event_dt'], y= relation['percent'], color='steelblue')
plt.title( 'Отношение событий между группами', fontsize=15)
plt.xlabel('дата события', fontsize=12),
plt.ylabel('Доля', fontsize=12)
plt.xticks(rotation=90)
plt.show()
Если первые 4 дня конверсия была одинаковой, то позже группа А стала показывать больше событий.
У меня на графике нет 30 декабря, странно, давайте посмотрим события в этот день
df[df['event_dt'] == '2020-12-30']
| user_id | group | ab_test | first_date | region | device | event_dt | event_name | details |
|---|
Событий нет, скорее всего 30/12 что то произошло и мы перестали фиксировать события.
Посмотрим на общее количество событий и количество событий по категориям
# подготовим таблицу
group_a = df[df['group'] == 'A'].pivot_table(index=['event_name','event_dt'], values='group', aggfunc='count').reset_index()
# строим график
fig = px.bar(group_a, x='event_dt', y='group',
width = 950, height = 550, color='event_name', text='group', opacity = 0.8)
fig.update_xaxes(tickangle=30)
fig.update_layout(
title='Общее количество событий и количество событий по категориям группа А',
xaxis_title= '',
yaxis_title='Количество событий')
fig.update_traces( textfont_size = 12 , textangle = 0, textposition = "outside" , cliponaxis = False )
fig.show()
# подготовим таблицу
group_b = df[df['group'] == 'B'].pivot_table(index=['event_name','event_dt'], values='group', aggfunc='count').reset_index()
# строим график
fig = px.bar(group_b, x='event_dt', y='group',
width = 950, height = 550, color='event_name', text='group', opacity = 0.8)
fig.update_xaxes(tickangle=30)
fig.update_layout(
title='Общее количество событий и количество событий по категориям группа B',
xaxis_title= '',
yaxis_title='Количество событий')
fig.update_traces( textfont_size = 12 , textangle = 0, textposition = "outside" , cliponaxis = False )
fig.show()
Это было видно и ранее, но я только сейчас обратил внимание, что в группе В активность выше в первые дни компании, поэтому и на графике отношения событий показатели и были в районе 1 а в группе А на вторую неделю начался рост.
С 21/12 в обоих группах пошел спад.
Посмотрим сколько пользователей приходили каждый 5 дней в наши группы
test_1 = df[df['first_date'] <= '2020-12-11']
test_1.groupby('group').agg({'event_name':'count', 'user_id':'nunique'}).reset_index()
| group | event_name | user_id | |
|---|---|---|---|
| 0 | A | 3004 | 501 |
| 1 | B | 2174 | 365 |
test = df[(df['first_date'] >= '2020-12-12') & (df['first_date'] < '2020-12-17')]
test.groupby('group').agg({'event_name':'count', 'user_id':'nunique'}).reset_index()
| group | event_name | user_id | |
|---|---|---|---|
| 0 | A | 6508 | 884 |
| 1 | B | 1547 | 271 |
test = df[(df['first_date'] >= '2020-12-17') & (df['first_date'] <= '2020-12-21')]
test.groupby('group').agg({'event_name':'count', 'user_id':'nunique'}).reset_index()
| group | event_name | user_id | |
|---|---|---|---|
| 0 | A | 9435 | 1362 |
| 1 | B | 1402 | 292 |
Из таблиц видно, что с течением времени сплитование стало больше отправлять в группу А, из-за этого и произошли видимые изменения на графике.
# подготовим таблицу
test_A = test_1[test_1['group'] == 'A'].pivot_table(index=['event_name','event_dt'], values='group', aggfunc='count').reset_index()
# строим график
fig = px.bar(test_A, x='event_dt', y='group',
width = 950, height = 550, color='event_name', text='group', opacity = 0.8)
fig.update_xaxes(tickangle=30)
fig.update_layout(
title='Общее количество событий и количество событий по категориям группа А',
xaxis_title= '',
yaxis_title='Количество событий')
fig.update_traces( textfont_size = 12 , textangle = 0, textposition = "outside" , cliponaxis = False )
fig.show()
# подготовим таблицу
test_B = test_1[test_1['group'] == 'B'].pivot_table(index=['event_name','event_dt'], values='group', aggfunc='count').reset_index()
# строим график
fig = px.bar(test_B, x='event_dt', y='group',
width = 950, height = 550, color='event_name', text='group', opacity = 0.8)
fig.update_xaxes(tickangle=30)
fig.update_layout(
title='Общее количество событий и количество событий по категориям группа B',
xaxis_title= '',
yaxis_title='Количество событий')
fig.update_traces( textfont_size = 12 , textangle = 0, textposition = "outside" , cliponaxis = False )
fig.show()
Мы отобрали пользователей, которые пришли к нам до 11/12, можно заметить, что поведение у них схоже.
Так же можем увидеть, что как правило пользователь приходит на сайт и в первые дни совершает все этапы воронки.
Посмотрим как регистрировались пользователи в эти 5 дней
sts = df[(df['first_date'] <= '2020-12-11') & (df['event_name'] == 'login')]['first_date']
sts = pd.to_datetime(sts).dt.day
sts.hist(bins=10, color='steelblue',figsize=(15, 5), ec="darkgrey")
plt.title('Распределение регситраций в первые 5 дней', fontsize=15)
plt.xlabel('', fontsize=12)
plt.ylabel('количество регистраций', fontsize=10)
plt.xticks(rotation = 90)
plt.show()
funnel_a = df[df['group'] == "A"].groupby('event_name').agg(count=('user_id','nunique')).reset_index()
funnel_a.rename(index={0:0, 1:2, 2:1, 3:3}, inplace= True )
funnel_a = funnel_a.sort_index()
funnel_a
| event_name | count | |
|---|---|---|
| 0 | login | 2747 |
| 1 | product_page | 1780 |
| 2 | product_cart | 824 |
| 3 | purchase | 872 |
funnel_b = df[df['group'] == "B"].groupby('event_name').agg(count=('user_id','nunique')).reset_index()
funnel_b.rename(index={0:0, 1:2, 2:1, 3:3}, inplace= True )
funnel_b = funnel_b.sort_index()
funnel_b
| event_name | count | |
|---|---|---|
| 0 | login | 927 |
| 1 | product_page | 523 |
| 2 | product_cart | 255 |
| 3 | purchase | 256 |
fig = go.Figure()
fig.add_trace(go.Funnel(name = 'A',
y = funnel_a['event_name'],
x = funnel_a['count'],
opacity = 0.9,
textposition = 'inside',
textinfo = 'value + percent previous + percent initial'))
fig.add_trace(go.Funnel(name = 'B',
y = funnel_b['event_name'],
x = funnel_b['count'],
opacity = 0.9,
textposition = 'auto',
textinfo = 'value + percent previous + percent initial'))
fig.update_layout(title_text='Воронка событий по группам')
fig.show()
Поведение пользователей в группе А выглядит лучше, больший процент пользователей совершает покупку.
Часть пользователей делает покупку пропуская страницу корзины
Ранее в ходе исследования, мы отметили адекватное распределение по группам в первые 5 дней теста, давайте построим воронку для этих пользователей
cut = df[df['first_date'] <= '2020-12-11']
funnel_at = cut[cut['group'] == "A"].groupby('event_name').agg(count=('user_id','nunique')).reset_index()
funnel_at.rename(index={0:0, 1:2, 2:1, 3:3}, inplace= True )
funnel_at = funnel_at.sort_index()
funnel_bt = cut[cut['group'] == "B"].groupby('event_name').agg(count=('user_id','nunique')).reset_index()
funnel_bt.rename(index={0:0, 1:2, 2:1, 3:3}, inplace= True )
funnel_bt = funnel_bt.sort_index()
fig = go.Figure()
fig.add_trace(go.Funnel(name = 'A',
y = funnel_at['event_name'],
x = funnel_at['count'],
opacity = 0.9,
textposition = 'inside',
textinfo = 'value + percent previous + percent initial'))
fig.add_trace(go.Funnel(name = 'B',
y = funnel_bt['event_name'],
x = funnel_bt['count'],
opacity = 0.9,
textposition = 'auto',
textinfo = 'value + percent previous + percent initial'))
fig.update_layout(title_text='Воронка событий по группам в первые 5 дней теста')
fig.show()
Если на общей воронке группа А выглядела лучше, то тут показатели схожи, группы ведут себя одинаково.
Говорить, что группа В покажет улучшение каждой метрики не менее, чем на 10% не представляется возможным.
Возможно если б сплиование работала корректно, общая воронка выглядела бы по другому
df[df['group'] == 'A']['event_name'].count()
18947
Вывод:
По ТЗ мы должны посмотреть поведение новых пользователей за 14 дней с момента регистрации, по факту в выборку попали пользователи которые прожили в приложении больше дней.
Убрав их мы увидели большой дисбаланс групп, в группе А в три раза больше пользователей.
Мы предположили, что сбой мог произойти на 5 -6 день сплитования, так как до этого распределение смотрелось более менее адекватно. Так же тест проводился до 04/01 , но по факту сбор данных прекратился 29/12, за 30 декабря уже нет ни каких данных.
Если смотреть общую воронку по группам, тио группа А выглядит предпочтительно, при этом если посмотреть воронку за первые 5 дней, группы смотрятся одинаково.
Так же на общую группу могло оказать влияние маркетинговое событие которое старануло 25/12.
В то время как не выборку из первых 5 дней влияние оно оказать не должно было…
Но 5 дней противоречит нашему ТЗ, мы не можем на него равняться, но можем утверждать, что результаты общего теста не объективны.
df.groupby('group').agg(count=('user_id','nunique')).reset_index()
| group | count | |
|---|---|---|
| 0 | A | 2747 |
| 1 | B | 928 |
В исследовании у меня было 927 в группе В, тут стало +1, пропусков и дубликатов нет. Найдем его
q = df[(df['group'] == "B") & (df['event_name'] == 'login')]['user_id'].unique()
w = df[(df['group'] == "B")]['user_id'].unique()
np.setdiff1d(w, q)
array(['5FF8B6AB257B404F'], dtype=object)
df[df['user_id'] == '5FF8B6AB257B404F']
| user_id | group | ab_test | first_date | region | device | event_dt | event_name | details | |
|---|---|---|---|---|---|---|---|---|---|
| 8523 | 5FF8B6AB257B404F | B | recommender_system_test | 2020-12-07 | EU | Android | 2020-12-07 | purchase | 4.99 |
Посмотрим на все действия от первой таблицы
final_ab_events[final_ab_events['user_id'] == '5FF8B6AB257B404F']
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 1411 | 5FF8B6AB257B404F | 2020-12-07 | purchase | 4.99 |
| 53685 | 5FF8B6AB257B404F | 2020-12-25 | purchase | 4.99 |
| 412914 | 5FF8B6AB257B404F | 2020-12-25 | login | NaN |
Странный клиент, хорошо, что единственный, не буду его учитывать
Сформируем таблицы
a_b_events_by_users = df.pivot_table(index='event_name', columns='group', values='user_id', aggfunc='nunique').reset_index()
a_b_events_by_users.rename(index={0:0, 1:2, 2:1, 3:3}, inplace= True )
a_b_events_by_users = a_b_events_by_users.sort_index()
a_b_events_by_users = a_b_events_by_users = a_b_events_by_users.iloc[1:4]
a_b_events_by_users
| group | event_name | A | B |
|---|---|---|---|
| 1 | product_page | 1780 | 523 |
| 2 | product_cart | 824 | 255 |
| 3 | purchase | 872 | 256 |
a_b_group = df[df['event_name'] == 'login'].groupby('group')['user_id'].nunique()
a_b_group
group A 2747 B 927 Name: user_id, dtype: int64
Проверим статистическую разницу долей z-критерием.. Сформулируем гипотезы
Сформулируем гипотезы.
Нулевая: Нет статистически значимого различия, нет оснований считать доли разными.
Альтернативная: Между долями есть значимая разница, отвергаем нулевую гипотезу.
Критический уровень статистической значимости укажем 0,05
Тк у нас сразу три теста, применим поправку Бонферрони для минимизации рисков
for i in a_b_events_by_users['event_name'].unique():
print(f'Статистически значимые различая между группами A и B для события {i}')
z_test(a_b_events_by_users.loc[a_b_events_by_users['event_name'] == i, 'A'],
a_b_events_by_users.loc[a_b_events_by_users['event_name'] == i, 'B'],
trials_1 = a_b_group.loc['A'],
trials_2 = a_b_group.loc['B'],
bonferroni_alpha = 3)
print()
print('---')
print()
Статистически значимые различая между группами A и B для события product_page [1780] [523] 2747 927 p-значение: [5.08436808e-06] значение alpha: 0.016666666666666666 Отвергаем нулевую гипотезу: между долями есть значимая разница --- Статистически значимые различая между группами A и B для события product_cart [824] [255] 2747 927 p-значение: [0.15034216] значение alpha: 0.016666666666666666 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными --- Статистически значимые различая между группами A и B для события purchase [872] [256] 2747 927 p-значение: [0.01847463] значение alpha: 0.016666666666666666 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными ---
Мы провели три теста, только в product_page отвергли нулевую гипотезу, это было видно и по воронке
Нельзя брать во внимание результаты данного А/В теста, тк был допущен ряд нарушений, из основного:
Так же рекомендую если есть возможность не допускать участи пользователей в нескольких тестах.